﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using OpenCvSharp;
using System.Collections.Specialized;
using System.Collections;
using System.Diagnostics;

namespace FacePrep
{
	class FaceCrop
	{

		protected ArrayList sampleList;
		protected ArrayList failedFaceDetect;
		protected ArrayList failedEyeDetect;
		protected ArrayList succeededList;
		protected ArrayList failedList;

		protected const string sampleListFilename = "filelist.txt";
		protected const string failedListFilename = "invalidList.txt";
		protected const string succeedListFilename = "validList.txt";

		protected string[] expression = new string[11];

		protected double scale;
		protected double haarFaceScaleFactor;
		protected double haarEyeScaleFactor;
		protected double templateWidthScale;
		protected double templateHeightScale;
		
		protected int totalCount;
		protected int hitCount;

		protected DirectoryInfo dataDir;
		protected DirectoryInfo outputDir; // where the cropped images stored

		// CvHaarClassifierCascade, cvHaarDetectObjects
		// 얼굴을 검출하기 위해서Haar분류기의 캐스케이드를 이용한다
		protected CvColor[] colors = new CvColor[]{
					new CvColor(0,0,255),
					new CvColor(0,128,255),
					new CvColor(0,255,255),
					new CvColor(0,255,0),
					new CvColor(255,128,0),
					new CvColor(255,255,0),
					new CvColor(255,0,0),
					new CvColor(255,0,255),
				};
		
		// draw circles on the image
		protected void drawCircle(CvSeq haarObject, IplImage image)
		{
			for (int i = 0; i < haarObject.Total; i++)
			{
				CvRect r = haarObject.GetSeqElem<CvRect>(i).Value;
				CvPoint center = new CvPoint()
				{
					X = Cv.Round((r.X + r.Width * 0.5)),
					Y = Cv.Round((r.Y + r.Height * 0.5))
				};
				int radius = Cv.Round((r.Width + r.Height) * 0.25);
#if (DEBUG && SHOWIMAGE)
				image.Rectangle(r, colors[i % 8]);
				//image.Circle(center, radius, colors[i % 8], 3, LineType.AntiAlias, 0);
				CvWindow.ShowImages(image);
			}
#else
			}
#endif			
		}

		// constructor
		public FaceCrop(double haarFaceScaleFactor, double haarEyeScaleFactor)
		{            
			scale=1.0;
			this.haarFaceScaleFactor = haarFaceScaleFactor;
			this.haarEyeScaleFactor = haarEyeScaleFactor;			

			totalCount = 0;
			hitCount = 0;

			sampleList = new ArrayList();
			failedFaceDetect = new ArrayList();
			failedEyeDetect = new ArrayList();
			failedList = new ArrayList();
			succeededList = new ArrayList();


			expression[0] = "centerlight";
			expression[1] = "glasses";
			expression[2] = "happy";
			expression[3] = "leftlight";
			expression[4] = "noglasses";
			expression[5] = "normal";
			expression[6] = "wink";
			expression[7] = "sad";
			expression[8] = "sleepy";
			expression[9] = "surprised";
			expression[10] ="'wink";



		}

		// constructor 2
		public FaceCrop(DirectoryInfo dataDir, DirectoryInfo outputDir, double haarFaceScaleFactor, double haarEyeScaleFactor)
			:this(haarFaceScaleFactor, haarEyeScaleFactor)
		{
			this.dataDir = dataDir;
			this.outputDir = outputDir;
		}

		//public void prepareData(DirectoryInfo databaseDir)
		public virtual void prepareData()
		{   
			// set the working directory
			DirectoryInfo projectDir = new DirectoryInfo(Directory.GetCurrentDirectory());
			try
			{                
				Directory.SetCurrentDirectory(dataDir.ToString());
			}
			catch (DirectoryNotFoundException e)
			{
				Console.WriteLine("Directory does not exist. {0}", e);
			}

			// get the sample file list to work with
			string imageFilename;
			using (StreamReader sr = new StreamReader(sampleListFilename))
			{
				while ((imageFilename = sr.ReadLine()) != null)
				{
					//imageFilename = Path.Combine(Directory.GetCurrentDirectory(), imageFilename);
					sampleList.Add(imageFilename);
				}
			}

			using (StreamReader sr = new StreamReader(failedListFilename))
			{
				while ((imageFilename = sr.ReadLine()) != null)
				{
					failedList.Add(imageFilename);
				}
			}


			using (StreamReader sr = new StreamReader(succeedListFilename))
			{
				while ((imageFilename = sr.ReadLine()) != null)
				{
					succeededList.Add(imageFilename);
				}
			}


			// change the working directory back to the project directory
			//Directory.SetCurrentDirectory(projectDir.ToString());
		}

		/**
		 * 새로 정의된 얼굴 영역을 기준으로 이미지에서 얼굴 영역을 잘라내어 새 이미지로 저장
		 */
		public virtual void cropFace()
		{            
			string facecroppedfilename;
			IplImage faceImage;
			CvMat facecropped;
			CvRect haarFaceROI;
			CvRect newFaceROI; // holder newly defined face ROI
			ArrayList filelist = null;



			if (failedList.Count == 0)
			{
				
				// use 'filelist.txt'
				failedList = (ArrayList)sampleList.Clone();
				filelist = (ArrayList)failedList.Clone();
				succeededList.Clear();
			}
			else
			{
				// use 'invalidList.txt' directly
				filelist = (ArrayList)failedList.Clone();
			}

			foreach (String imageFilename in filelist)
			{               
				IplImage sample = new IplImage(imageFilename, LoadMode.Color);

				//Console.Clear();
				//Console.WriteLine("Total count: {0}", ++totalCount);

				// detect faces
				if (detectFace(ref sample, out faceImage, out haarFaceROI))
				{

					// get the new face roi
					if (getNewFaceRoi(ref faceImage, ref haarFaceROI, out newFaceROI))  // save the newly defined face as an image
					{
						facecroppedfilename = imageFilename; // define cropped filename
						facecroppedfilename = Path.Combine(outputDir.ToString(), facecroppedfilename);

						try
						{
							Cv.GetSubRect(faceImage, out facecropped, newFaceROI);
							Cv.SaveImage(facecroppedfilename, facecropped);
							hitCount++;
							if (!UpdateList(failedList, imageFilename, '+'))
							{
								System.Console.WriteLine("Update Error");
							}
						}
						catch (OpenCVException e)
						{
							failedEyeDetect.Add(imageFilename);
							Console.WriteLine("OpenCV exception: {0}", e);
						}

						

					}
					else
					{
						failedEyeDetect.Add(imageFilename);
						
						//Console.WriteLine("For sample {0}, no eyes detected.", imageFilename);
						continue;
					}

				}
				else                    
				{
					failedFaceDetect.Add(imageFilename);
					//Console.WriteLine("For sample {0}, no face detected.", imageFilename);
					continue; 
				}      
			}//end of foreach

			Wrapup();
			Console.WriteLine("Hit Count: {0}", hitCount);
			Console.WriteLine("Face Detection Failure Count: {0}", failedFaceDetect.Count);
			Console.WriteLine("Eye Detection Failure Count: {0}", failedEyeDetect.Count);
		}

		/**
		 * 샘플 이미지로부터 얼굴 영역을 찾는다
		 */
		protected virtual bool detectFace(ref IplImage sample, out IplImage faceImage, out CvRect haarFaceROI)
		{
			// initialize outputs
			faceImage = new IplImage();
			haarFaceROI = new CvRect();

			/**
			 * 얼굴 검출용의 화상의 생성
			 */
			
			// 1. convert the original image to grayscale
			IplImage sampleGrayscale = new IplImage(sample.Size, BitDepth.U8, 1);  
			Cv.CvtColor(sample, sampleGrayscale, ColorConversion.BgrToGray);

			// 2. resize the grayscale image
			IplImage sampleScaled = new IplImage(new CvSize(Cv.Round(sample.Width / scale),
					Cv.Round(sample.Height / scale)), BitDepth.U8, 1);            
			Cv.Resize(sampleGrayscale, sampleScaled, Interpolation.Linear);

			// 3. Equalize histogram
			Cv.EqualizeHist(sampleScaled, sampleScaled);

			/*
			 * 얼굴의 검출
			 */
			using (CvMemStorage storage = new CvMemStorage())
			using (CvHaarClassifierCascade cascade = CvHaarClassifierCascade.FromFile(@"..\haartrained\haarcascade_frontalface_default.xml"))
			{
				storage.Clear();
				
				Stopwatch watch = Stopwatch.StartNew();
				// 얼굴로 인식된 영역(들)을 얻음
				CvSeq faces = Cv.HaarDetectObjects(sampleScaled, cascade, storage, haarFaceScaleFactor, 4, 0, new CvSize(30, 30));
				watch.Stop();
				
				// face detection result validation
				if (faces.Total != 1)
				{
					//Console.WriteLine("Not a valid detection");
					return false;
				}
				else
				{
					// set the detected face region
					haarFaceROI = faces.GetSeqElem<CvRect>(0).Value;
					Cv.SetImageROI(sampleScaled, haarFaceROI);

					faceImage = sampleScaled;
					
					//Console.WriteLine("detection time = {0}msn", watch.ElapsedMilliseconds);
					return true;
				}
			}
		}

		/**
		 * 눈을 기준으로 새 얼굴 영역을 정의
		 */
		protected virtual bool getNewFaceRoi(ref IplImage faceImage, ref CvRect haarFaceROI, out CvRect newFaceROI)
		{
			const int min_neighbors = 3;
			newFaceROI = new CvRect();
						
			/**
			 * 새 얼굴 영역의 기준을 위해 눈을 찾은 다음 두 눈의 중심과 거리를 사용
			 */
			// set the ROI for eye region
			Cv.SetImageROI(faceImage,                    /* the source image */
							new CvRect(
								haarFaceROI.X,            /* x = start from leftmost */
								haarFaceROI.Y + (int)(haarFaceROI.Height / 5.5), /* y = a few pixels from the top */
								haarFaceROI.Width,        /* width = same width with the face */
								(int)(haarFaceROI.Height / 3.0)    /* height = 1/3 of face height */
							)
						);

			CvPoint eyeCenter;
			using (CvHaarClassifierCascade cascade = CvHaarClassifierCascade.FromFile(@"..\haartrained\haarcascade_mcs_eyepair_small.xml"))
			using (CvMemStorage storage = new CvMemStorage())
			{
				storage.Clear();
				CvSeq eyes = Cv.HaarDetectObjects(faceImage, cascade, storage, haarEyeScaleFactor, min_neighbors, 0, new CvSize(20, 20));

				// eye detection validation
				if (eyes.Total != 1)
				{
					System.Console.WriteLine("" + eyes.Total);
					drawCircle(eyes, faceImage);
					return false;
				}

				// set the eye center
				else
				{

					// get the CvRect type from the eyes

					// haarcascade_eye.xml 사용할때 필요
					/*CvRect eyeRect1 = eyes.GetSeqElem<CvRect>(0).Value;
					CvPoint eye1 = new CvPoint()
					{
						X = Cv.Round((eyeRect1.X + eyeRect1.Width * 0.5) ),
						Y = Cv.Round((eyeRect1.Y + eyeRect1.Height * 0.5) )
					};

					CvRect eyeRect2 = eyes.GetSeqElem<CvRect>(1).Value;
					CvPoint eye2 = new CvPoint()
					{
						X = Cv.Round((eyeRect2.X + eyeRect2.Width * 0.5) ),
						Y = Cv.Round((eyeRect2.Y + eyeRect2.Height * 0.5) )
					};

					eyeCenter = GetCenter(eye1, eye2);
					

					

					// get the coordinate of the CvRect
					int diffH = Math.Abs(eye1.X - eye2.X); // 가로축 차이값
					int diffV = Math.Abs(eye1.Y - eye2.Y); // 세로축 차이값
					*/


					// haarcascade_mcs_eyepair_small.xml 사용할때 필요
					CvRect eyeRect1 = eyes.GetSeqElem<CvRect>(0).Value;
					drawCircle(eyes, faceImage);
					CvPoint eye1 = new CvPoint()
					{
						X = Cv.Round((eyeRect1.X + eyeRect1.Width * 0.5) ),
						Y = Cv.Round((eyeRect1.Y + eyeRect1.Height * 0.5) )
					};

					eyeCenter = eye1;

					// eye detect validation
					//if (diffH < 40 || diffV > 20) // eye detection invalidated
					//{
						//drawCircle(eyes, faceImage);
					//    return false;
					//}
					//else // eye detection validated
					{
						//drawCircle(eyes, faceImage);
						//resetROI
						faceImage.ResetROI();
						

						//int eyeDiff = Math.Abs(eye1.X - eye1.X);
						//int newFaceWidth = (int)(eyeDiff * 1.5);
						//int newFaceHeight = eyeDiff * 3;
						int newFaceWidth = (int)(faceImage.Width / templateWidthScale);
						int newFaceHeight = (int)(faceImage.Height / templateHeightScale);

						// redefine the face ROI
						newFaceROI = new CvRect()
						{
							X = eyeCenter.X + haarFaceROI.X - (int)newFaceWidth / 2,
							Y = eyeCenter.Y + haarFaceROI.Y + (int)(haarFaceROI.Height / 5.5) - (int)newFaceHeight / 3,
							//Width = faceWidth,
							//Height = faceHeight
							//X= eyeCenter.X - newFaceWidth/2,
							//Y=eyeCenter.Y-newFaceHeight/3,
							Width=newFaceWidth,
							Height=newFaceHeight
						};                        

						return true;
					}
				}
			}
		}



		/* function that saves file lists into a text file */
		protected virtual void Wrapup()
		{

			int i;
			
			// 'filelist.txt' doesn't change

			// updates 'validList.txt' using succeededList
			using (StreamWriter sw = new StreamWriter(succeedListFilename, false))
			{
				//strArray = new string[succeededList.Count];
				//succeededList.CopyTo(strArray, 0);
				//Array.Sort(strArray);
				succeededList.Sort();

				for (i = 0; i < succeededList.Count; i++)
				{
					sw.WriteLine(succeededList[i]);
				}
				sw.Flush();
				sw.Close();
			}

			using (StreamWriter sw = new StreamWriter(failedListFilename, false))
			{
				//strArray = new string[failedList.Count];
				//failedList.CopyTo(strArray, 0);
				//Array.Sort(strArray);
				failedList.Sort();

				for (i = 0; i < failedList.Count; i++)
				{
					sw.WriteLine(failedList[i]);
				}
				sw.Flush();
				sw.Close();
			}

		   

			// updates 'invalidList.txt' using failedList
		}

		/* updates filelist */
		protected virtual bool UpdateList(ArrayList collection, string filename, char option)
		{
			int index;

			switch (option)
			{
					// moves the filename to the succeededList from the failedList
				case '+':
					if ((index = failedList.IndexOf(filename)) >= 0)
					{
						if (!succeededList.Contains(filename))
						{
							failedList.RemoveAt(index);
							succeededList.Add(filename);
							return true;
						}
						else
						{
							return false;
						}

					}
					else
					{
						// failedList doesn't contain 'filename'
						return false;
					}
					


				// moves the filename to the failedList from the succeededList
				case '-':
					break;

			}

			return false;
		}


		protected CvPoint GetCenter(CvPoint point1, CvPoint point2)
		{
			CvPoint pointCenter = new CvPoint();

			//x
			if (point1.X > point2.X) // if the eye1 is right eye
			{
				pointCenter.X = point2.X + (point1.X - point2.X) / 2;
			}
			else
			{                // if the eye2 is right eye
				pointCenter.X = point1.X + (point2.X - point1.X) / 2;
			}

			//y
			if (point1.Y > point2.Y) // if the eye1 is right eye
			{
				pointCenter.Y = point2.Y + (point1.Y - point2.Y) / 2;


			}
			else
			{                // if the eye2 is right eye
				pointCenter.Y = point1.Y + (point2.Y - point1.Y) / 2;
			}

			return pointCenter;

		}
	}
}
